package org.xx.armory.swing;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.xx.armory.commons.Version;

import javax.management.JMException;
import javax.management.MBeanServer;
import javax.management.ObjectName;
import javax.swing.*;
import java.awt.Image;
import java.awt.SplashScreen;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.UncheckedIOException;
import java.lang.management.ManagementFactory;
import java.util.Locale;
import java.util.Properties;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Supplier;

import static org.xx.armory.commons.SysUtils.expandPath;
import static org.xx.armory.commons.SysUtils.forceMkDirs;
import static org.xx.armory.commons.SysUtils.openInputStream;
import static org.xx.armory.commons.Validators.notBlank;
import static org.xx.armory.commons.Validators.notNull;
import static org.xx.armory.swing.ComponentUtils.getFrameBounds;
import static org.xx.armory.swing.ComponentUtils.setFrameBounds;
import static org.xx.armory.swing.UIUtils.assertUIThread;

/**
 * 桌面应用程序实例。
 */
public final class Application
        implements ApplicationMBean {
    private static final AtomicReference<Application> INSTANCE = new AtomicReference<>();
    private final Logger logger = LoggerFactory.getLogger(Application.class);
    private final String name;
    private final Object mainFrameNotifier;
    private final Properties settings;
    private volatile boolean mainFrameDisposed;
    private String title;
    private Image icon;
    private String productName;
    private String copyright;
    private Version version;
    private volatile JFrame mainFrame;
    private ApplicationListener listener;

    /**
     * 使用指定的主窗体构造应用程序实例。
     *
     * @param args
     *         命令行参数。
     * @param name
     *         应用程序的名字。
     * @throws IllegalArgumentException
     *         如果参数{@code mainFrame}是{@code null}。
     */
    public Application(
            String[] args,
            String name
    ) {
        Thread.setDefaultUncaughtExceptionHandler(UIUtils::defaultDumpThrowable);

        this.name = notBlank(name, "name").trim();
        this.settings = new Properties();
        this.mainFrameNotifier = new Object();

        if (!INSTANCE.compareAndSet(null, this)) {
            throw new IllegalStateException("cannot constructor more application");
        }

        final Locale systemLocale = new Locale(System.getProperty("user.language"), System.getProperty("user.country"));
        Locale.setDefault(systemLocale);
        logger.trace("default locale: {}", systemLocale);

        loadProperties();

        registerMBean();
    }

    /**
     * 获取当前的应用程序对象。
     *
     * @return 当前的应用程序对象。
     */
    public static Application current() {
        return INSTANCE.get();
    }

    /**
     * 获取当前的主窗体。
     *
     * @return 当前的主窗体。
     */
    public static JFrame mainFrame() {
        return current().mainFrame;
    }

    /**
     * 获取当前的配置。
     *
     * @return 当前的配置。
     */
    public static Properties settings() {
        return current().settings;
    }

    /**
     * 获取当前应用程序的标题。
     *
     * @return 当前应用程序的标题。
     */
    public static String title() {
        return current().title;
    }

    /**
     * 获取当前应用程序的图标。
     *
     * @return 当前应用程序的图标。
     */
    public static Image icon() {
        return current().icon;
    }

    /**
     * 获取当前应用程序的名字。
     *
     * @return 当前应用程序的名字。
     */
    public static String productName() {
        return current().productName;
    }

    /**
     * 获取当前应用程序的版权。
     *
     * @return 当前应用程序的版权。
     */
    public static String copyright() {
        return current().copyright;
    }

    /**
     * 获取当前应用程序的版本。
     *
     * @return 当前应用程序的版本。
     */
    public static Version version() {
        return current().version;
    }

    /**
     * 将应用程序自身注册为JMXBean。
     */
    private void registerMBean() {
        final MBeanServer server = ManagementFactory.getPlatformMBeanServer();
        try {
            ObjectName objectName = new ObjectName(getClass().getPackage().getName() + ":name=" + this.name);
            server.registerMBean(this, objectName);
        } catch (JMException ex) {
            logger.warn("cannot register JMX Bean", ex);
        }
    }

    /**
     * 设置事件监听器。
     *
     * @param listener
     *         事件监听器。
     */
    public void setListener(
            ApplicationListener listener
    ) {
        this.listener = listener;
    }

    /**
     * 启动应用程序。
     *
     * @param frameSupplier
     *         创建应用程序的主窗体的方法，此方法会在UI线程中被调用，创建主窗体。
     * @throws IllegalArgumentException
     *         如果参数{@code frame}是{@code null}。
     */
    public final void run(
            Supplier<JFrame> frameSupplier
    ) {
        notNull(frameSupplier, "frameSupplier");

        // 启动应用程序时首先显示Splash屏幕。
        // java -splash:resources/splash.gif
        final SplashScreen splash = SplashScreen.getSplashScreen();
        if (splash == null) {
            logger.warn("SplashScreen.getSplashScreen() returned null");
        } else {
            splash.update();
        }

        // TODO: 解析命令行，处理默认参数。

        loadSettings();

        // 主窗体监视信号
        synchronized (this.mainFrameNotifier) {
            onStartUp();

            this.mainFrameDisposed = false;

            // 启动用户界面。
            SwingUtilities.invokeLater(() -> {
                this.mainFrame = frameSupplier.get();
                this.showMainFrame();
            });

            try {
                // 等待主窗体监视信号被触发。
                while (!this.mainFrameDisposed) {
                    this.mainFrameNotifier.wait();
                }
            } catch (InterruptedException ex) {
                System.exit(1);
            }
        }

        onExit();

        saveSettings();

        System.exit(0);
    }

    /**
     * 关闭应用程序。
     */
    public final void shutdown() {
        // 通过关闭主窗体来关闭应用程序。
        this.mainFrame.dispose();
    }

    /**
     * 触发APPLICATION_START_UP事件。
     */
    protected void onStartUp() {
        if (this.listener != null) {
            listener.startUp(new ApplicationEvent(this, ApplicationEvent.APPLICATION_START_UP));
        }
    }

    /**
     * 触发APPLICATION_EXIT事件。
     */
    protected void onExit() {
        if (this.listener != null) {
            listener.exit(new ApplicationEvent(this, ApplicationEvent.APPLICATION_EXIT));
        }
    }

    /**
     * 显示主窗体并进入事件处理循环。
     */
    private void showMainFrame() {
        assertUIThread();

        // 从当前配置中恢复主窗体。
        restoreMainFrame();

        this.mainFrame.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);

        this.mainFrame.addWindowListener(new WindowAdapter() {
            @Override
            public void windowOpened(
                    WindowEvent event
            ) {
                // 主窗口已打开，关闭Splash屏幕（如果存在）。
                final SplashScreen splash = SplashScreen.getSplashScreen();
                if (splash != null) {
                    splash.close();
                }
            }

            @Override
            public void windowClosing(
                    WindowEvent event
            ) {
                // 将主窗体保存到配置。
                preserveMainFrame();
            }

            @Override
            public void windowClosed(
                    WindowEvent event
            ) {
                synchronized (Application.this.mainFrameNotifier) {
                    // 触发主窗体关闭的信号。
                    Application.this.mainFrameDisposed = true;
                    Application.this.mainFrameNotifier.notifyAll();
                }
            }
        });

        // 显示主窗体。
        this.mainFrame.setVisible(true);
    }

    /**
     * 加载应用程序级别的属性，比如应用程序名称，图标等等。
     */
    private void loadProperties() {
        this.title = UIControllers.GLOBAL.getMessage("application-title");
        this.icon = UIControllers.GLOBAL.getImage("application-icon");
        this.productName = UIControllers.GLOBAL.getMessage("application-product-name");
        this.copyright = UIControllers.GLOBAL.getMessage("application-copyright");
        this.version = UIControllers.GLOBAL.getVersion();

        logger.trace("application.title={}", this.title);
        logger.trace("application.icon={}", this.icon);
        logger.trace("application.productName={}", this.productName);
        logger.trace("application.copyright={}", this.copyright);
        logger.trace("application.version={}", this.version);
    }

    private File getSettingsFile() {
        final File dir = new File(expandPath("~/." + this.name));
        forceMkDirs(dir);
        return new File(dir, "settings.properties");
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void loadSettings() {
        logger.trace("load application default settings");

        try (final InputStream stream = openInputStream("classpath://default-settings.properties")) {
            this.settings.load(stream);
        } catch (IOException ex) {
            logger.warn("cannot load default settings", ex);
        }

        logger.trace("load application settings...");

        final File file = getSettingsFile();
        if (!file.isFile() || !file.canRead()) {
            logger.warn("cannot open settings {} for read", file.getAbsolutePath());
            return;
        }

        try (final InputStream stream = new BufferedInputStream(new FileInputStream(file))) {
            this.settings.load(stream);
        } catch (IOException ex) {
            throw new UncheckedIOException(ex);
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void saveSettings() {
        logger.trace("save application settings...");

        final File file = getSettingsFile();
        if (file.exists()) {
            if (file.isDirectory() || !file.canWrite()) {
                logger.warn("cannot open settings {} for write", file.getAbsolutePath());
                return;
            }
        }

        try (final OutputStream stream = new BufferedOutputStream(new FileOutputStream(file))) {
            this.settings.store(stream, null);
        } catch (IOException ex) {
            throw new UncheckedIOException(ex);
        }
    }

    /**
     * 恢复主窗体的尺寸。
     */
    private void restoreMainFrame() {
        final FrameBounds frameBounds = new FrameBounds(this.settings.getProperty("main-frame"));
        setFrameBounds(this.mainFrame, frameBounds);
    }

    /**
     * 保存主窗体的尺寸。
     */
    private void preserveMainFrame() {
        final FrameBounds frameBounds = getFrameBounds(this.mainFrame);
        this.settings.setProperty("main-frame", frameBounds.toString());
    }
}
